Jena, 15.05.2019
Markus Harrer, Software Development Analyst
Twitter: @feststelltaste
Blog: feststelltaste.de

"Datenanalysen auf nem Mac."
=> Belastbare Erkenntnisse mittels Fakten liefern
"The aim of science is to seek the simplest explanations of complex facts."
=> Neue Erkenntnisse verständlich herausarbeiten
"Jemand, der mehr Ahnung von Statistik
hat als ein Softwareentwickler
und mehr Ahnung von Softwareentwicklung
als ein Statistiker."
Data Science: Perfect match!
Alles was aus der Entwicklung und dem Betrieb der Softwaresysteme so anfällt:
Data Science ❤ Software Data = Software Analytics
=> von der Frage über die Daten zur Erkenntnis!
[(Daten + Code + Ergebnis) pro gedanklichen Schritt] + komplette Automatisierung
Schlüsselelement: Computational Notebooks
Jupyter funktioniert und integriert sich auch mit
Schildert Ausgangslage, Analyseideen und Heuristiken.
Meta-Ziel: Alles mal sehen anhand eines einfachen Show-Cases.
Frage 1: Gibt es Module mit Teammonopole?
Unsere Heurisik: Ändert ein Team hauptsächlich Module?
from ozapfdis import git
log = git.log_numstat("../../../dropover/")
log.head()
| additions | deletions | file | sha | timestamp | author | |
|---|---|---|---|---|---|---|
| 0 | 191.0 | 0.0 | backend/pom-2016-07-16_04-40-56-752.xml | 8c686954 | 2016-07-22 17:43:38 | Michael |
| 1 | 1.0 | 1.0 | backend/src/test/java/at/dropover/scheduling/i... | 97c6ef96 | 2016-07-16 09:51:15 | Markus |
| 2 | 19.0 | 3.0 | backend/src/main/webapp/app/widgets/gallery/js... | 3f7cf92c | 2016-07-16 09:07:31 | Markus |
| 3 | 1.0 | 1.0 | backend/src/main/webapp/app/widgets/gallery/vi... | 3f7cf92c | 2016-07-16 09:07:31 | Markus |
| 4 | 3.0 | 4.0 | backend/src/main/java/at/dropover/files/intera... | ec85fe73 | 2016-07-16 08:12:29 | Chris |
Was haben wir hier eigentlich?
log.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 2403 entries, 0 to 2402 Data columns (total 6 columns): additions 2353 non-null float64 deletions 2353 non-null float64 file 2403 non-null object sha 2403 non-null object timestamp 2403 non-null datetime64[ns] author 2403 non-null object dtypes: datetime64[ns](1), float64(2), object(3) memory usage: 112.7+ KB
1 DataFrame (~ programmierbares Excel-Arbeitsblatt), 6 Series (= Spalten), 2403 Rows (= Einträge)
Wir machen nur mit Java-Produktionscode weiter
# %load javaprod.py
java = log.copy()
java = java[java['file'].str.startswith("backend/src/main/java")]
java = java[~java['file'].str.contains("package-info.java")]
java.head()
| additions | deletions | file | sha | timestamp | author | |
|---|---|---|---|---|---|---|
| 4 | 3.0 | 4.0 | backend/src/main/java/at/dropover/files/intera... | ec85fe73 | 2016-07-16 08:12:29 | Chris |
| 36 | 3.0 | 2.0 | backend/src/main/java/at/dropover/scheduling/i... | bfea33b8 | 2016-07-16 02:02:02 | Markus |
| 44 | 23.0 | 1.0 | backend/src/main/java/at/dropover/scheduling/i... | ab9ad48e | 2016-07-16 00:50:20 | Chris |
| 47 | 68.0 | 6.0 | backend/src/main/java/at/dropover/files/intera... | 0732e9cb | 2016-07-16 00:27:20 | Chris |
| 53 | 7.0 | 3.0 | backend/src/main/java/at/dropover/framework/co... | ba1fd215 | 2016-07-15 22:51:46 | Chris |
=> Mehr Perspektiven auf ein Problem möglich
Wir extrahiere Modulnamen aus den Dateinamen.
java['module'] = java['file'].str.split("/").str[6]
java.head()
| additions | deletions | file | sha | timestamp | author | module | |
|---|---|---|---|---|---|---|---|
| 4 | 3.0 | 4.0 | backend/src/main/java/at/dropover/files/intera... | ec85fe73 | 2016-07-16 08:12:29 | Chris | files |
| 36 | 3.0 | 2.0 | backend/src/main/java/at/dropover/scheduling/i... | bfea33b8 | 2016-07-16 02:02:02 | Markus | scheduling |
| 44 | 23.0 | 1.0 | backend/src/main/java/at/dropover/scheduling/i... | ab9ad48e | 2016-07-16 00:50:20 | Chris | scheduling |
| 47 | 68.0 | 6.0 | backend/src/main/java/at/dropover/files/intera... | 0732e9cb | 2016-07-16 00:27:20 | Chris | files |
| 53 | 7.0 | 3.0 | backend/src/main/java/at/dropover/framework/co... | ba1fd215 | 2016-07-15 22:51:46 | Chris | framework |
Wir ordnen bei uns Autoren zu Teams zu.
Schritt 1: Weitere Datenquelle einlesen.
import pandas as pd
team = pd.read_excel("../dataset/Teamorganisation.xlsx", index_col=0)
team
| team | |
|---|---|
| name | |
| Markus | A |
| Chris | B |
| Michael | C |
Wir ordnen bei uns Committer zu Teams zu.
Schritt 2: Datenquellen joinen
java_team = java.join(team, on="author")
java_team.head()
| additions | deletions | file | sha | timestamp | author | module | team | |
|---|---|---|---|---|---|---|---|---|
| 4 | 3.0 | 4.0 | backend/src/main/java/at/dropover/files/intera... | ec85fe73 | 2016-07-16 08:12:29 | Chris | files | B |
| 36 | 3.0 | 2.0 | backend/src/main/java/at/dropover/scheduling/i... | bfea33b8 | 2016-07-16 02:02:02 | Markus | scheduling | A |
| 44 | 23.0 | 1.0 | backend/src/main/java/at/dropover/scheduling/i... | ab9ad48e | 2016-07-16 00:50:20 | Chris | scheduling | B |
| 47 | 68.0 | 6.0 | backend/src/main/java/at/dropover/files/intera... | 0732e9cb | 2016-07-16 00:27:20 | Chris | files | B |
| 53 | 7.0 | 3.0 | backend/src/main/java/at/dropover/framework/co... | ba1fd215 | 2016-07-15 22:51:46 | Chris | framework | B |
= > Feingranulare Daten sinnvoll zusammenfassen
Wir zählen die Änderungen aller Klassen pro Modul und Team.
changes = java_team.groupby(["module", "team"])[["sha"]].count()
changes.head()
| sha | ||
|---|---|---|
| module | team | |
| comment | A | 41 |
| B | 76 | |
| C | 1 | |
| creator | A | 14 |
| B | 33 |
Wir berechnen alle erfolgten Änderungen pro Modul...
changes['all'] = changes.groupby("module").transform("sum")
changes.head()
| sha | all | ||
|---|---|---|---|
| module | team | ||
| comment | A | 41 | 118 |
| B | 76 | 118 | |
| C | 1 | 118 | |
| creator | A | 14 | 54 |
| B | 33 | 54 |
...und damit die Änderungsverhältnisse pro Team.
changes["ratio"] = changes["sha"] / changes["all"]
changes.head()
| sha | all | ratio | ||
|---|---|---|---|---|
| module | team | |||
| comment | A | 41 | 118 | 0.347458 |
| B | 76 | 118 | 0.644068 | |
| C | 1 | 118 | 0.008475 | |
| creator | A | 14 | 54 | 0.259259 |
| B | 33 | 54 | 0.611111 |
Wir bauen uns ein Balkendiagramm mit den Verhältnissen der Teamänderungen.
changes['ratio'].unstack().plot.bar(stacked=True);
Wir haben jetzt 80% der notwendigen Befehle für Datenanalysen in der Softwareentwicklung abgedeckt!
Frage 2: Wie gut passt die Modularisierung zur Arbeitweise der Teams?
Unsere Heuristik: Werden fachliche Komponenten zusammengehörig geändert?
Unsere Ausgangsbasis
java.head()
| additions | deletions | file | sha | timestamp | author | module | team | |
|---|---|---|---|---|---|---|---|---|
| 4 | 3.0 | 4.0 | backend/src/main/java/at/dropover/files/intera... | ec85fe73 | 2016-07-16 08:12:29 | Chris | files | B |
| 36 | 3.0 | 2.0 | backend/src/main/java/at/dropover/scheduling/i... | bfea33b8 | 2016-07-16 02:02:02 | Markus | scheduling | A |
| 44 | 23.0 | 1.0 | backend/src/main/java/at/dropover/scheduling/i... | ab9ad48e | 2016-07-16 00:50:20 | Chris | scheduling | B |
| 47 | 68.0 | 6.0 | backend/src/main/java/at/dropover/files/intera... | 0732e9cb | 2016-07-16 00:27:20 | Chris | files | B |
| 53 | 7.0 | 3.0 | backend/src/main/java/at/dropover/framework/co... | ba1fd215 | 2016-07-15 22:51:46 | Chris | framework | B |
Wir zeigen für jeden Commit die geänderten Dateien.
java['changed'] = 1 # markiere Änderungen
commit_matrix = java.pivot('file', 'sha', 'changed').fillna(0)
commit_matrix.iloc[:5,50:55]
| sha | 3597d8a2 | 3b70ea7e | 3d3be4ca | 3e4ae692 | 429b3b32 |
|---|---|---|---|---|---|
| file | |||||
| backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/CommentData.java | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/GetCommentRequestModel.java | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/GetCommentResponseModel.java | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
Wir berechnen den Abstand zwischen den vorgenommmenen Commits pro Datei (=Vektor)...
from sklearn.metrics.pairwise import cosine_distances
dis_matrix = cosine_distances(commit_matrix)
dis_matrix[:5,:5]
array([[0. , 0.29289322, 0.5 , 0.18350342, 0.29289322],
[0.29289322, 0. , 0.29289322, 0.1339746 , 0.5 ],
[0.5 , 0.29289322, 0. , 0.59175171, 0.29289322],
[0.18350342, 0.1339746 , 0.59175171, 0. , 0.42264973],
[0.29289322, 0.5 , 0.29289322, 0.42264973, 0. ]])
...und machen die Darstellung schöner...
class_name = commit_matrix.index.str.split("/").str[-1].str.split(".").str[0]
dis_df = pd.DataFrame(dis_matrix, class_name, class_name)
dis_df.iloc[:5,:2]
| file | AddCommentRequestModel | ChangeCommentRequestModel |
|---|---|---|
| file | ||
| AddCommentRequestModel | 0.000000 | 0.292893 |
| ChangeCommentRequestModel | 0.292893 | 0.000000 |
| CommentData | 0.500000 | 0.292893 |
| GetCommentRequestModel | 0.183503 | 0.133975 |
| GetCommentResponseModel | 0.292893 | 0.500000 |
Wir brechen nun die Matrix auf 2D herunter
from sklearn.manifold import MDS
model = MDS(dissimilarity='precomputed', random_state=0)
dis_2d = model.fit_transform(dis_df)
dis_2d_df = pd.DataFrame(dis_2d, commit_matrix.index, ["x", "y"])
dis_2d_df.head(3)
| x | y | |
|---|---|---|
| file | ||
| backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java | -0.525928 | 0.450702 |
| backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java | -0.568260 | 0.215280 |
| backend/src/main/java/at/dropover/comment/boundary/CommentData.java | -0.527468 | 0.347568 |
Wir fügen eine Sicht auf die Module hinzu.
dis_2d_df['module'] = dis_2d_df.index.str.split("/").str[6]
dis_2d_df.head()
| x | y | module | |
|---|---|---|---|
| file | |||
| backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java | -0.525928 | 0.450702 | comment |
| backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java | -0.568260 | 0.215280 | comment |
| backend/src/main/java/at/dropover/comment/boundary/CommentData.java | -0.527468 | 0.347568 | comment |
| backend/src/main/java/at/dropover/comment/boundary/GetCommentRequestModel.java | -0.558567 | 0.262028 | comment |
| backend/src/main/java/at/dropover/comment/boundary/GetCommentResponseModel.java | -0.403657 | 0.498037 | comment |
Wir erzeugen uns eine interaktive Grafik.
Helferlein: An Unifying Software Integrator
from ausi import pygal
xy = pygal.create_xy_chart(dis_2d_df, "module")
xy.render_in_browser()
file://C:/Users/MARKUS~1/AppData/Local/Temp/tmp_r7if0oj.html
Frage 3: Gibt es eine alternative Modularisierung?
Unsere Heuristik: Wie würde sich das System rein nach seinen Änderungen strukturieren?
Unsere Ausgangsbasis
commit_matrix.iloc[:5,50:55]
| sha | 3597d8a2 | 3b70ea7e | 3d3be4ca | 3e4ae692 | 429b3b32 |
|---|---|---|---|---|---|
| file | |||||
| backend/src/main/java/at/dropover/comment/boundary/AddCommentRequestModel.java | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/ChangeCommentRequestModel.java | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/CommentData.java | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/GetCommentRequestModel.java | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| backend/src/main/java/at/dropover/comment/boundary/GetCommentResponseModel.java | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
Wir nutzen hierarchisches Clustering, um anhand der Änderungsmuster alternative Modulestrukturen zu erkennen...
from sklearn.cluster import AgglomerativeClustering
clustering = AgglomerativeClustering()
model = clustering.fit(commit_matrix)
model
AgglomerativeClustering(affinity='euclidean', compute_full_tree='auto',
connectivity=None, linkage='ward', memory=None, n_clusters=2,
pooling_func='deprecated')
...und visualisieren das Ergebnis.
from ausi.scipy import plot_dendrogram
plot_dendrogram(model, labels=commit_matrix.index)
https://upload.wikimedia.org/wikipedia/commons/thumb/7/71/062-exploding-head.svg/600px-062-exploding-head.svg.png
Abhängigkeitsanalyse nach EVA-Prinzip mit OZAPFDIS, pandas und AUSI
Oder auch: Softwarewelt -> Data Science Welt -> Softwarewelt
1. Es gibt unglaublich viele Quellen für Analysen
2. Problemanalysen mit Standard-Data-Science-Werkzeugen einfach möglich
3. Wer mehr will, bekommt auch mehr!
=> von der Frage über die Daten zur Erkenntnis!
Markus Harrer
innoQ Deutschland GmbH
E-Mail: markus.harrer@innoq.com
Twitter: @feststelltaste
Blog: feststelltaste.de
